package game.console;

import game.gfx.IRenderer;
import game.util.Logger;
import game.util.Util;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public abstract class AbstractConsole implements IRenderer
{
    private static final int             MAX_COMMAND_LENGTH    = 100;

    private final char[]                 CONSOLE_ALLOWED_CHARS = " 0123456789.,-_?!\"'$%&()[]{}ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\\/#+*=".toCharArray();
    
    private final String                 LINE_BEGIN            = ">";
    private final String                 LINE_CARET            = "_";
    // buffer
    private List<String>                 consoleBuffer         = new ArrayList<String>();
    private StringBuffer                 lineBuffer            = new StringBuffer();
    private int                          caretPosition         = 0;
    private boolean                      handlingKeyEvent      = false;
    // commands
    private List<String>                 commandBuffer         = new ArrayList<String>();
    private Map<String, IConsoleCommand> commands              = new HashMap<String, IConsoleCommand>();
    private Map<String, String>          synonyms              = new HashMap<String, String>();
    private int                          commandPointer        = 0;
    
    private Dimension                    size;
    private int                          x;
    private int                          y;
    
    private int                          margin                = 2;
    private int                          firstLine             = 0;
    // args
    private Pattern                      regex;
    
    private boolean                      logConsole            = true;
    
    protected AbstractConsole()
    {
        // regex
        String regexChars = "A-Za-z0-9\\+\\-\\_\\=\\(\\)\\[\\]\\{\\}\\.\\,\\/";
        regex = Pattern.compile("((\\\"+[" + regexChars + " ]+\"+)|[" + regexChars + "]+)");
        // help
        addConsoleCommand(new IConsoleCommand() 
        {
            @Override
            public String getCommand()
            {
                return "?";
            }
            
            @Override
            public String[] getSynonym()
            {
                return new String[] { "help" };
            }
            
            @Override
            public String getInfo()
            {
                return "this list";
            }
            
            @Override
            public IConsoleStream fireCommand(String[] args)
            {
                return new AbstractIteratorConsoleStream<IConsoleCommand>()
                {
                    @Override
                    protected IConsoleCommand[] getValues()
                    {
                        return commands.values().toArray(new IConsoleCommand[commands.size()]);
                    }

                    @Override
                    protected String getMessage(IConsoleCommand current)
                    {
                        StringBuilder s = new StringBuilder(current.getCommand());
                        if (current.getSynonym() != null && current.getSynonym().length > 0)
                        {
                            for (String string : current.getSynonym())
                            {
                                s.append(" | ");
                                s.append(string);
                            }
                        }
                        s.append(" - ");
                        s.append(current.getInfo());
                        return s.toString();
                    }
                };
            }
        });
        // exit
        addConsoleCommand(new IConsoleCommand() {
            
            @Override
            public String[] getSynonym()
            {
                return new String[] { "DIE!!!" };
            }
            
            @Override
            public String getInfo()
            {
                return "exit to os";
            }
            
            @Override
            public String getCommand()
            {
                return "exit";
            }
            
            @Override
            public IConsoleStream fireCommand(String[] args)
            {
                return new MessageAndActionConsoleStream("GOODBYE")
                {
                    @Override
                    public void run()
                    {
                        new Thread(new Runnable() {
                            
                            @Override
                            public void run()
                            {
                                long start = System.currentTimeMillis();
                                //sleep 5s
                                do
                                {
                                    try
                                    {
                                        Thread.sleep(500);
                                    }
                                    catch (InterruptedException ex)
                                    {
                                        // --
                                    }
                                }
                                while(System.currentTimeMillis() - start < 1000);
                                // shutdown
                                System.exit(0);
                            }
                        }, "EXIT").start();
                    }
                };
            }
        });
        // enable logging
        addConsoleCommand(new IConsoleCommand()
        {
            
            @Override
            public String[] getSynonym()
            {
                return null;
            }
            
            @Override
            public String getInfo()
            {
                return "enable/disable logging (+|-)";
            }
            
            @Override
            public String getCommand()
            {
                return "log";
            }
            
            @Override
            public IConsoleStream fireCommand(String[] args)
            {
                String msg = "illegal arguments " + args.length;
                if (args.length == 2)
                {
                    if ("+".equals(args[1]) || "-".equals(args[1]))
                    {
                        logConsole = "+".equals(args[1]);
                        msg = "logging " + (logConsole ? "enabled" : "disabled");
                    }
                    else
                    {
                        msg = "illegal argument: " + args[1];
                    }
                }
                else if (args.length == 1)
                {
                    msg = "logging is " + (logConsole ? "enabled" : "disabled");
                }
                return new MessageConsoleStream(msg);
            }
        });
    }
    
    public void fireKeyEvent(KeyEvent e)
    {
        if (handlingKeyEvent == false)
        {
            handlingKeyEvent = true;
            boolean isAllowedChar = false;
            for (char allowedChar : CONSOLE_ALLOWED_CHARS)
            {
                if (allowedChar == e.getKeyChar())
                {
                    isAllowedChar = true;
                    break;
                }
            }
            if (isAllowedChar)
            {
                if (lineBuffer.length() < MAX_COMMAND_LENGTH)
                {
                    char c = e.getKeyChar();
                    lineBuffer.insert(caretPosition++, c);
                }
                else
                {
                    String msg = "MAX-COMMAND-LENGTH LIMIT REACHED";
                    if (isLastInConsoleBuffer(msg) == false)
                    {
                        consoleBufferOut(msg);
                    }
                }
            }
            else
            {
                switch (e.getKeyCode())
                {
                    case KeyEvent.VK_DELETE:
                    {
                        if (lineBuffer.length() > caretPosition)
                        {
                            lineBuffer.deleteCharAt(caretPosition);
                        }
                        break;
                    }
                    case KeyEvent.VK_BACK_SPACE:
                    {
                        if (lineBuffer.length() > 0 && lineBuffer.length() >= caretPosition && caretPosition > 0)
                        {
                            lineBuffer.deleteCharAt(--caretPosition);
                        }
                        break;
                    }
                    case KeyEvent.VK_ENTER:
                    {
                        fireConsole();
                        break;
                    }
                    case KeyEvent.VK_UP:
                    {
                        if (commandPointer > 0)
                        {
                            commandPointer--;
                            lineBuffer.replace(0, lineBuffer.length(), commandBuffer.get(commandPointer));
                            caretPosition = lineBuffer.length();
                        }
                        break;
                    }
                    case KeyEvent.VK_DOWN:
                    {
                        if (commandPointer < commandBuffer.size())
                        {
                            commandPointer++;
                            if (commandPointer < commandBuffer.size())
                            {
                                lineBuffer.replace(0, lineBuffer.length(), commandBuffer.get(commandPointer));
                            }
                            caretPosition = lineBuffer.length();
                        }
                        break;
                    }
                    case KeyEvent.VK_LEFT:
                    {
                        if (caretPosition > 0)
                        {
                            caretPosition--;
                        }
                        break;
                    }
                    case KeyEvent.VK_RIGHT:
                    {
                        if (caretPosition == 0 || caretPosition < lineBuffer.length())
                        {
                            caretPosition++;
                        }
                        break;
                    }    
                    case KeyEvent.VK_PAGE_UP:
                    {
                        firstLine++;
                        break;
                    }
                    case KeyEvent.VK_PAGE_DOWN:
                    {
                        firstLine--;
                        break;
                    }
                    default:
                    {
                        break;
                    }
                }
            }
            handlingKeyEvent = false;
        }
    }
    
    private void consoleBufferOut(String line)
    {
        consoleBuffer.add(line);
        if (logConsole)
        {
            Logger.trace(line);
        }
    }
    
    private boolean isLastInConsoleBuffer(String msg)
    {
        return consoleBuffer.isEmpty() || consoleBuffer.get(consoleBuffer.size() - 1).equals(msg) == false;
    }
    
    public void addConsoleCommand(IConsoleCommand c)
    {
        String command = c.getCommand();
        if (command == null || command.isEmpty() || command.contains(" "))
        {
            throw new IllegalArgumentException("command must be a single word! invalid command: " + command);
        }
        if (commands.containsKey(command))
        {
            throw new IllegalArgumentException("command already exists: " + command);
        }
        commands.put(command, c);
        String[] synonyms = c.getSynonym();
        if (synonyms != null && synonyms.length > 0)
        {
            for (String synonym : synonyms)
            {
                if (synonym == null || synonym.isEmpty() || synonym.contains(" "))
                {
                    throw new IllegalArgumentException("synonym must be a single word! invalid synonym: " + synonym);
                }
                if (commands.containsKey(synonym))
                {
                    throw new IllegalArgumentException("command already exists: " + command);
                }
                if (this.synonyms.containsKey(synonym))
                {
                    throw new IllegalArgumentException("command already exists: " + command);
                }
                this.synonyms.put(synonym, command);
            }
        }
    }
    
    private void fireConsole()
    {
        String commandLine = lineBuffer.toString();
        consoleBufferOut(LINE_BEGIN + commandLine);
        commandBuffer.add(commandLine);
        commandPointer = commandBuffer.size();
        // get command from commandLine
        String command = commandLine;
        if (command.contains(" "))
        {
            command = command.substring(0, command.indexOf(" "));
        }
        // check if its a synonym
        if (synonyms.containsKey(command))
        {
            command = synonyms.get(command);
        }
        // get command-object
        if (commands.containsKey(command))
        {
            try
            {
                // create args
                List<String> args = new ArrayList<String>();
                Matcher m = regex.matcher(commandLine);
                while (m.find())
                {
                    String arg = m.group(1);
                    args.add(arg);
                }
                // get stream
                IConsoleCommand c = commands.get(command);
                IConsoleStream input = c.fireCommand(args.toArray(new String[args.size()]));
                String line = null;
                while ((line = input.readLine()) != null)
                {
                    consoleBufferOut(line);
                }
                if (input instanceof IAction)
                {
                    ((IAction) input).run();
                }
            }
            catch (Exception ex)
            {
                consoleBufferOut(ex.getClass().getSimpleName() + ": " + ex.getLocalizedMessage());
                consoleBufferOut(Util.toString(ex));
            }
        }
        else
        {
            consoleBufferOut("Unknown command: " + command);
        }
        clearLineBuffer();
    }
    
    protected void clearLineBuffer()
    {
        lineBuffer.delete(0, lineBuffer.length());
        firstLine = 0;
        caretPosition = 0;
    }
    
    public void setXY(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
    
    public void setSize(int width, int height)
    {
        this.size = new Dimension(width,height);
    }
    
    protected Dimension getSize()
    {
        return size;
    }
    
    @Override
    public void render(Graphics g)
    {
        // weißer hintergrund
        g.setColor(Color.WHITE);
        g.fillRect(x, y, getSize().width, getSize().height + 5);
        // schwarze schrift
        g.setColor(Color.BLACK);
        g.setFont(new Font("Monospaced", Font.PLAIN, 14));
        FontMetrics fm = g.getFontMetrics();
        // Anzahl Zeilen max
        int maxLines = getSize().height / fm.getHeight();
        int maxChars = (getSize().width / fm.charWidth(' ')) - (margin * margin);
        // output
        List<String> output = new ArrayList<>();
        for (int i = consoleBuffer.size() - 1; i >= 0 && output.size() <= maxLines + firstLine; i--)
        {
            String s = consoleBuffer.get(i);
            if (s.length() > maxChars)
            {
                List<String> tOutput = new ArrayList<String>();
                for (int y = 0; y < s.length(); y += maxChars)
                {
                    int length = y + maxChars;
                    if (length >= s.length())
                    {
                        length = s.length() - 1;
                    }
                    tOutput.add(s.substring(y, length));
                }
                Collections.reverse(tOutput);
                output.addAll(tOutput);
            }
            else
            {
                output.add(s);
            }
        }
        if (firstLine < 0 || firstLine >= output.size())
        {
            firstLine = 0;
        }
        // remove skipped lines
        if (firstLine > 0)
        {
            for (int i = 0; i < firstLine; i++)
            {
                if (output.size() >= maxLines)
                {
                    output.remove(0);
                }
                else
                {
                    firstLine = i;
                }
            }
        }
        // remove start that can not be displayed
        while (output.size() >= maxLines)
        {
            output.remove(output.size() - 1);
        }
        Collections.reverse(output);
        // create line
        StringBuilder lineGenerator = new StringBuilder();
        lineGenerator.append(LINE_BEGIN);
        // generate
        String line = lineBuffer.toString();
        if (line.isEmpty())
        {
            lineGenerator.append(LINE_CARET);
        }
        else
        {
            for (int i = 0; i < line.length(); i++)
            {
                if (i == caretPosition)
                {
                    lineGenerator.append(LINE_CARET);
                }
                lineGenerator.append(line.charAt(i));
            }
        }
        if (caretPosition > 0 && caretPosition == line.length())
        {
            lineGenerator.append(LINE_CARET);
        }
        output.add(lineGenerator.toString());
        // rendern
        int l = 0;
        for (String s : output)
        {
            g.drawString(s, margin, ++l * fm.getHeight());
        }
    }
}
